{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fe12c203-e6a6-452c-a655-afb8a03a4ff5",
   "metadata": {},
   "source": [
    "# End of week 1 exercise\n",
    "\n",
    "To demonstrate your familiarity with OpenAI API, and also Ollama, build a tool that takes a technical question,  \n",
    "and responds with an explanation. This is a tool that you will be able to use yourself during the course!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "c1070317-3ed9-4659-abe3-828943230e03",
   "metadata": {},
   "outputs": [],
   "source": [
    "# imports\n",
    "\n",
    "import os\n",
    "from dotenv import load_dotenv\n",
    "from IPython.display import Markdown, display, update_display\n",
    "from openai import OpenAI"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4a456906-915a-4bfd-bb9d-57e505c5093f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# constants\n",
    "\n",
    "MODEL_GPT = 'gpt-4o-mini'\n",
    "MODEL_LLAMA = 'llama3.2'\n",
    "\n",
    "load_dotenv(override=True)\n",
    "api_key = os.getenv('OPENAI_API_KEY')\n",
    "\n",
    "if api_key and api_key.startswith('sk-proj-') and len(api_key) > 0:\n",
    "    print('API key looks good')\n",
    "else:\n",
    "    print('API key is incorrect')\n",
    "\n",
    "openai = OpenAI()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "a8d7923c-5f28-4c30-8556-342d7c8497c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# set up environment\n",
    "\n",
    "system_prompt = \"\"\"\n",
    "You are a technical assistant that answers technical questions and\n",
    "explains them in a way they can easily be understood even by a dummy\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3f0d0137-52b0-47a8-81a8-11a90a010798",
   "metadata": {},
   "outputs": [],
   "source": [
    "# here is the question; type over this to ask something new\n",
    "\n",
    "question = \"\"\"\n",
    "Please explain what this code does and why:\n",
    "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "60ce7000-a4a5-4cce-a261-e75ef45063b4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "Sure! Let's break down the code step by step to understand what it does.\n",
       "\n",
       "### Code Breakdown:\n",
       "```python\n",
       "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n",
       "```\n",
       "\n",
       "1. **`books`**: This is presumably a list (or any iterable) of dictionaries, where each dictionary represents a book. Each book dictionary is expected to have an `\"author\"` key.\n",
       "\n",
       "2. **List Comprehension**: The part `{book.get(\"author\") for book in books if book.get(\"author\")}` is called a set comprehension. It goes through each `book` in the `books` iterable.\n",
       "\n",
       "   - **`book.get(\"author\")`**: This tries to retrieve the value associated with the `\"author\"` key from each `book` dictionary. If the key does not exist, `get()` returns `None` instead of raising an error.\n",
       "\n",
       "   - **`if book.get(\"author\")`**: This condition filters out any entries that do not have an `\"author\"`. If `book.get(\"author\")` is `None` (which would happen if the key is missing) or evaluates to `False` for any other reason, that book will not be included in the next steps.\n",
       "\n",
       "3. **Creating a Set**: The outer curly braces `{}` indicate that this is creating a set (not a list). A set in Python is a collection that automatically removes duplicate entries. So even if multiple books have the same author, that author's name will only appear once in the set.\n",
       "\n",
       "4. **`yield from`**: This is used in the context of a generator function. By using `yield from`, you are effectively yielding each item from the set you created in the previous step. \n",
       "\n",
       "   - When this generator function is called, it will provide one author at a time to the caller until all authors in the set are exhausted.\n",
       "\n",
       "### What It Does:\n",
       "- The code collects the unique authors from a list of books dictionaries, ignoring any that do not have an author.\n",
       "- It then \"yields\" these unique authors one by one as a generator.\n",
       "\n",
       "### Why Use This Code:\n",
       "- **Efficiency**: It gathers unique authors, which can be helpful in scenarios where you want to process or list authors without duplicates.\n",
       "- **Simplicity**: Using `yield from` allows handling potentially large collections of authors without loading all of them into memory at once, making it scalable for large datasets.\n",
       "\n",
       "### Summary:\n",
       "This line of code is creating a generator that yields unique authors from a list of book dictionaries, while ignoring any entries that do not have an author. It’s a concise way to filter and collect specific data from a collection."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Get gpt-4o-mini to answer, with streaming\n",
    "\n",
    "messages = [\n",
    "    {'role': 'system', 'content': system_prompt},\n",
    "    {'role': 'user', 'content': question}\n",
    "]\n",
    "\n",
    "stream = openai.chat.completions.create(\n",
    "    model=MODEL_GPT,\n",
    "    messages=messages,\n",
    "    stream=True\n",
    ")\n",
    "\n",
    "response = ''\n",
    "display_handle = display(Markdown(''), display_id=True)\n",
    "for chunk in stream:\n",
    "    response += chunk.choices[0].delta.content or ''\n",
    "    update_display(Markdown(response), display_id=display_handle.display_id)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "8f7c8ea8-4082-4ad0-8751-3301adcf6538",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "Let's break down this line of code:\n",
       "\n",
       "**What is it doing?**\n",
       "\n",
       "This piece of code is using a Python syntax called \"generator expression\" or \"list comprehension with yield\". It's doing the following:\n",
       "\n",
       "- Retrieving author names from a list of books (`books`).\n",
       "- Only considering books with existing `author` information.\n",
       "- Yielding (one by one) the author names that satisfy the conditions.\n",
       "\n",
       "**The actual code:**\n",
       "\n",
       "```python\n",
       "yield from {book.get(\"author\") for book in books if book.get(\"author\")}\n",
       "```\n",
       "\n",
       "Let's understand each part of it:\n",
       "\n",
       "1. `{...}`: This is called a dict comprehension, which creates a dictionary from key-value pairs.\n",
       "\n",
       "2. `for book in books`: This part iterates over the `books` list to process each book's information.\n",
       "\n",
       "3. `if book.get(\"author\")`: Before yielding each author name, it checks if that particular book has an \"author\" key with some value (not necessarily a string).\n",
       "\n",
       "4. `yield from {...}`: It uses Python's `yield from` keyword to delegate to yields (produces) values provided by the inner dictionary comprehension `{book.get(\"author\") for book in books if book.get(\"author\")}`.\n",
       "\n",
       "5. `.get()` and `.if`: The `book.get('author')` would directly get the author's value if it is present, otherwise `dict.get()` would find a default value to `None`, then check condition using `if`\n",
       "\n",
       "Here's why:\n",
       "\n",
       "- Generator expressions in functions (like this code snippet or many other places) are super helpful because they reduce unnecessary computations:\n",
       "   \n",
       "   Instead of having to manually generate and return author names when the `author` data is needed, you can create a generator expression that does all these computations ahead of time, returning one value at a time.\n",
       "\n",
       "- `yield from expression`:\n",
       "\n",
       "    The syntax has its source in some older Python features like `yield from Generator`. This version allows you to define a generator (an object which defines the iteration with next() and yield).\n",
       "\n",
       "Let's write this code snippet with more Pythonic ways and clear the doubt for it\n",
       "\n",
       "```python\n",
       "def authors():\n",
       "  def get_author(book):\n",
       "      if book.get('author'):\n",
       "        return book['author']\n",
       "    else:\n",
       "        return None\n",
       "  if __name__ == '__main__':\n",
       "    from data_classes import books\n",
       "    def iterate_over_books():\n",
       "        for book in books:\n",
       "          author = get_author(book)\n",
       "          if not author is None: # instead of  if 'get()' which finds the value or returns None\n",
       "            yield author\n",
       "\n",
       "    final_books = iterate_over_books()\n",
       "   # do something with your \"yield\" iter object. We could make a list out of it for instance \n",
       "#   # print(your_iter_result)\n",
       "    return final_books \n",
       "\n",
       "def final_book_objects():\n",
       "  from data_classes import books  # Define the class 'books'\n",
       "  \n",
       "# If we have some book objects and wish to extract only that author value - this is our code\n",
       "#       book_obj = Book(title='A',author= 'J')\n",
       "\n",
       "  def generate_list_of_author_names(): \n",
       "    final_books = []\n",
       "   for book in books:  \n",
       "      final_book = { \"book\":book}\n",
       "      for key, value in final_book.items():\n",
       "        if value == \"author\":\n",
       "          author = value\n",
       "    #   yield from author\n",
       "         final_books.append(author)    # append to the list of final books with 'author' appended to it.\n",
       "    return final_books\n",
       "        \n",
       "  result = generate_list_of_author_names()\n",
       "   # do something with your iter object. We could make a list out of it for instance \n",
       "#   # print(result)  \n",
       "  return result \n",
       "\n",
       "iterating_code = final_book_objects()\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Get Llama 3.2 to answer\n",
    "\n",
    "messages = [\n",
    "    {'role': 'system', 'content': system_prompt},\n",
    "    {'role': 'user', 'content': question}\n",
    "]\n",
    "\n",
    "OLLAMA_BASE_URL = 'http://localhost:11434/v1'\n",
    "ollama = OpenAI(base_url=OLLAMA_BASE_URL, api_key='ollama')\n",
    "\n",
    "stream = ollama.chat.completions.create(\n",
    "    model=MODEL_LLAMA,\n",
    "    messages=messages,\n",
    "    stream=True\n",
    ")\n",
    "\n",
    "response = ''\n",
    "display_handle = display(Markdown(''), display_id=True)\n",
    "for chunk in stream:\n",
    "    response += chunk.choices[0].delta.content or ''\n",
    "    update_display(Markdown(response), display_id=display_handle.display_id)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9ecab4ce",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
